package bubbleindex;

import Jama.Matrix;
import com.nativelibs4java.opencl.CLBuildException;
import com.nativelibs4java.opencl.CLContext;
import com.nativelibs4java.opencl.CLDevice;
import com.nativelibs4java.opencl.CLKernel;
import com.nativelibs4java.opencl.CLPlatform;
import com.nativelibs4java.opencl.CLProgram;
import com.nativelibs4java.opencl.CLQueue;
import com.nativelibs4java.opencl.JavaCL;
import static info.yeppp.Core.Multiply_V64fV64f_V64f;
import static info.yeppp.Core.Subtract_V64fV64f_V64f;
import static info.yeppp.Math.Log_V64f_V64f;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 *
 * @author ttrott
 */
public class BuildIndex {
     
    volatile static boolean Stop;
    public static List<Float> ResultList;
    static List<CLPlatform> platforms;
    static List<CLContext> contexts;
    static List<Integer> maxComputeUnits;
    static List<CLQueue> queues;
    static List<CLProgram> programs;    
    static List<CLKernel> addFloatsKernels;
    static List<ByteOrder> byteOrders;
    public static String src;

    public static void Create(int DAILY_DATA_SIZE, int NUMBER_OF_INDICES) {
        
        if (!BubbleIndex.forceCPU) {
            platforms = new ArrayList<>();
            contexts = new ArrayList<>();
            maxComputeUnits = new ArrayList<>();
            queues = new ArrayList<>();
            programs = new ArrayList<>();    
            addFloatsKernels = new ArrayList<>();
            byteOrders = new ArrayList<>();

            //First try to find GPU, if no GPU then catch and proceed with CPU
            try {
                CLPlatform[] platformsArray = JavaCL.listGPUPoweredPlatforms();
                for (CLPlatform platform : platformsArray) {
                    BubbleIndex.displayOutput(platform.getName(), false);
                    platforms.add(platform);
                    CLDevice[] allDevices = platform.listGPUDevices(true);
                    for (CLDevice device : allDevices) {
                        BubbleIndex.displayOutput("Device: " + device.getName(), false);
                        BubbleIndex.displayOutput("Driver:" + device.getDriverVersion(), false);
                        CLContext context = JavaCL.createContext(null, device);
                        contexts.add(context);
                        maxComputeUnits.add(device.getMaxComputeUnits());
                        CLQueue queue = context.createDefaultQueue();
                        queues.add(queue);
                        BubbleIndex.displayOutput("GPU context created with " + device.getName() + 
                                " :: Driver " + device.getDriverVersion(), false);

                        CLProgram program = context.createProgram(src);
                        programs.add(program);
                        CLKernel kernel = program.createKernel("hq_derivative");
                        addFloatsKernels.add(kernel);
                        ByteOrder byteOrder = context.getByteOrder();
                        byteOrders.add(byteOrder);
                    }
                }
                    
                for (int i = 0; i < NUMBER_OF_INDICES; i++){			

                    Stop = false;
                    ResultList = new ArrayList<>();
                    List<String> DateList = new ArrayList<>();
                    List<String> DataList = new ArrayList<>();

                    BubbleIndex.PreviousFilePath = Indices.userDir + "ProgramData" + 
                            Indices.filePathSymbol + BubbleIndex.CategoryName + Indices.filePathSymbol + 
                            BubbleIndex.SelectionName + Indices.filePathSymbol + BubbleIndex.SelectionName + 
                            Integer.toString(BubbleIndex.BacktestDayLengths[i]) + "days.csv";

                    if (DAILY_DATA_SIZE - BubbleIndex.BacktestDayLengths[i] <= 1) {
                        Stop = true;
                        continue;
                    }

                    int START_INDEX;

                    if (InputData.checkForFile(BubbleIndex.PreviousFilePath)) {

                        InputData.ReadValues(BubbleIndex.PreviousFilePath, DataList, 
                            DateList, true, true);

                        int UpdateLength = BubbleIndex.DailyPriceDateList.size() 
                                - updateDateMatch(DateList) - 1;

                        if (Stop) {
                            System.out.println("Dates in the previous file: " + 
                                    BubbleIndex.PreviousFilePath + " do not match. Code 028.");
                            BubbleIndex.displayOutput("Dates in the previous file: " + 
                                    BubbleIndex.PreviousFilePath + " do not match.", false);
                            continue;
                        }

                        START_INDEX = DAILY_DATA_SIZE - BubbleIndex.BacktestDayLengths[i] 
                                - UpdateLength;   
                    }

                    else {
                        START_INDEX = 0;         
                    }

                    final int numBatches = (int) Math.ceil((DAILY_DATA_SIZE - BubbleIndex.BacktestDayLengths[i] - START_INDEX) / 500.0);

                    for (int batch = 0; batch < numBatches; batch++) {
                        int batchStartIndex = batch * 500 + START_INDEX;
                        int batchEndIndex = (batch + 1) * 500 + START_INDEX;
                        ExecutorService executor = Executors.newFixedThreadPool(BubbleIndex.ThreadNumber);

                        List<Callable<Float>> callables = new ArrayList<>();

                        createCallables(contexts, programs, addFloatsKernels, queues, byteOrders, maxComputeUnits, 
                                callables, batchStartIndex, Math.min(DAILY_DATA_SIZE - BubbleIndex.BacktestDayLengths[i], batchEndIndex), i);
                        try {
                            List<Future<Float>> results = executor.invokeAll(callables);

                            for (Future<Float> futures : results) {
                                ResultList.add(futures.get());
                            }

                            executor.shutdown();
                            executor.awaitTermination(5, TimeUnit.SECONDS);
                        } catch (InterruptedException | ExecutionException ex) {
                            executor.shutdownNow();
                           // executor.awaitTermination(10, TimeUnit.SECONDS);
                            System.out.println("Exception. Code 011." + ex);
                            //releaseGPUs(platforms, contexts, programs, addFloatsKernels, queues);
                            Stop = true;
                            break;
                        }
                    }

                    if (!Stop) {
                        String Name = BubbleIndex.SelectionName + 
                            BubbleIndex.BacktestDayLengths[i] + "days.csv";

                        try {
                            ExportData.WriteCSV(ResultList, DAILY_DATA_SIZE - BubbleIndex.BacktestDayLengths[i],
                                    Name, InputData.checkForFile(BubbleIndex.PreviousFilePath));
                        } catch (IOException ex) {
                            System.out.println("Error writing export file. Code 011." + ex);
                        }
                    }
                }
                
                releaseGPUs(platforms, contexts, programs, addFloatsKernels, queues);
            
            } catch (CLBuildException th) {
                
                System.out.println("CLBuildException. Code 012." + th);            
                BubbleIndex.displayOutput("No GPU found. Using CPU.", false);    
                
                releaseGPUs(platforms, contexts, programs, addFloatsKernels, queues);
                
                noGPU.Create(DAILY_DATA_SIZE, NUMBER_OF_INDICES);
            
            //} catch (InterruptedException ex) {
                
            //    System.out.println("Exception. Code 011." + ex);
            //    releaseGPUs(platforms, contexts, programs, addFloatsKernels, queues);
            }
        } 
        
        else {
            noGPU.Create(DAILY_DATA_SIZE, NUMBER_OF_INDICES);
        }
    }
    
    private static void createCallables(List<CLContext> contexts, List<CLProgram> programs, List<CLKernel> addFloatsKernels,
            List<CLQueue> queues, List<ByteOrder> byteOrders, List<Integer> maxComputeUnits, 
            List<Callable<Float>> callables, int START, int SIZE, int INDEX) {

        int numberGPUContexts = contexts.size();

        if (numberGPUContexts == 0) {
            Stop = true;
        }
        
        else if (numberGPUContexts == 1) {
            for (int j = START; j < SIZE; j++) {
                callables.add(new MyCallable(j, BubbleIndex.BacktestDayLengths[INDEX], contexts.get(0), queues.get(0),
                    programs.get(0), addFloatsKernels.get(0), byteOrders.get(0)));   
            }  
        }
        
        else {
            int totalComputeUnitsSum = 0;
            for (Integer computeUnits : maxComputeUnits) {
                totalComputeUnitsSum = totalComputeUnitsSum + computeUnits;
            }
            
            Map<CLContext, Double> contextComputeMapping = new LinkedHashMap<>();
            Map<CLContext, Integer> contextOrder = new HashMap<>();
            double randomSum = 0.0;
            int index = 0;
            
            for (CLContext context : contexts) {
                int computeUnits = context.getDevices()[0].getMaxComputeUnits();//only one device per context

                randomSum = Math.min(randomSum + computeUnits * 1.0 / totalComputeUnitsSum, 1.0);
                contextComputeMapping.put(context, randomSum);
                contextOrder.put(context, index);
                index++;
            }
            
            Random random = new Random(System.currentTimeMillis());
            
            for (int j = START; j < SIZE; j++) {
                double randomValue = random.nextDouble();
                
                for (Map.Entry<CLContext, Double> entry : contextComputeMapping.entrySet()) {
                    
                    if (randomValue <= entry.getValue()) {
                       int contextIndex = contextOrder.get(entry.getKey());
                        callables.add(new MyCallable(j, BubbleIndex.BacktestDayLengths[INDEX], contexts.get(contextIndex), queues.get(contextIndex),
                            programs.get(contextIndex), addFloatsKernels.get(contextIndex), byteOrders.get(contextIndex)));  
                        break;
                    }
                }    
            } 
        }                 
    }

    public static int updateDateMatch(List<String> DateList) {
        final String finalValue = DateList.get(DateList.size() - 1);

        for (int i = 0; i < BubbleIndex.DailyPriceDateList.size(); i++) {
            if (finalValue.equals(BubbleIndex.DailyPriceDateList.get(i))) {    
                return i;
            }
        }
        
        /*Since previous file exists but no dates matched, must be error... 
        start from beginning*/
        Stop = true;
        return 0;                        
    }
    
     /**
     * DataReverse method takes an array and reverses the linear order of the
     * array.
     * @param Data The array to be reversed
     * @param SIZE The size of the array
     */   
    public static void DataReverse(double[] Data, final int SIZE) {
        
        double[] Temp = new double[SIZE];
        for (int i = 0; i < SIZE; i++) {
            Temp[i] = Data[SIZE - 1 - i];
        }
        
        System.arraycopy(Temp, 0, Data, 0, SIZE);      
    }  

    /**
     * 
     * @param SelectedData
     * @param NumberOfDays 
     */
    public static void Normalize(double[] SelectedData, int NumberOfDays) {
		
        double[] ReturnsData = new double[NumberOfDays];
        ReturnsData[0] = 0.0;
        double[] tempOne = new double[NumberOfDays];
        double[] tempTwo = new double[NumberOfDays];
        double[] tempThree = new double[NumberOfDays];
        double[] tempFour = new double[NumberOfDays];
        double[] tempFive = new double[NumberOfDays];
        
        for (int i = 1; i < NumberOfDays; i++) {
            tempOne[i] = SelectedData[i];
            tempTwo[i] = SelectedData[i-1];
            tempThree[i] = 1.0 / SelectedData[i-1];
        }
        
        Subtract_V64fV64f_V64f(tempOne, 0, tempTwo, 0, tempFour, 0, NumberOfDays);
        
        Multiply_V64fV64f_V64f(tempFour, 0, tempThree, 0, tempFive, 0, NumberOfDays);
        
        System.arraycopy(tempFive, 1, ReturnsData, 1, NumberOfDays - 1);

        //Declare and initialize array of normalized price values
        SelectedData[0] = 100.0;

        for (int i = 1; i < NumberOfDays; i++) {
            SelectedData[i] = SelectedData[i-1] * ReturnsData[i] + SelectedData[i-1];
        }

        Log_V64f_V64f(SelectedData, 0, tempFive, 0, NumberOfDays);
        
        System.arraycopy(tempFive, 0, SelectedData, 0, NumberOfDays);

    }

    /**
     * The LinearFit method solves the b = Ax matrix for the x vector. In other
     * words this is a linear regression to fit the equation to the log prices.
	 * The equation is y = b + Ax1 + Bx2 where x1 and x2 are TimeValues_M_power
	 * and LogCosTimeValues respectively.
     * @param Data The array containing the log prices
     * @param TimeValues_M_Power Array of time values raised to the M power
     * @param LogCosTimeValues Array of logcos time values
     * @param Coef Array containing the models fitted coefficients
     * @param SIZE Size of the data window (days)
     */
    public static void LinearFit(double[] Data, double[] TimeValues_M_Power,
            double[] LogCosTimeValues, double[] Coef, final int SIZE){
            
    	double[][] Array = new double[SIZE][3];
            
    	for (int i = 0; i < SIZE; i++) {
    	    Array[i][0] = 1.0;
    	    Array[i][1] = TimeValues_M_Power[i];
    	    Array[i][2] = LogCosTimeValues[i];
        }
    
        Matrix A = new Matrix(Array);
        Matrix b = new Matrix(Data, SIZE);
        Matrix x = A.solve(b);
            
        for (int i = 0; i < 3; i++) {
    	    Coef[i] = x.get(i,0);
        }
    }     
    
    private static void releaseGPUs(List<CLPlatform> platforms, List<CLContext> contexts, 
            List<CLProgram> programs, List<CLKernel> addFloatsKernels, List<CLQueue> queues) {

        for (CLProgram program : programs) {
            program.release();
        }

        for (CLKernel kernel : addFloatsKernels) {
            kernel.release();
        }

        for (CLQueue queue : queues) {
            queue.flush();
            queue.release();
        }

        for (CLContext context : contexts) {
            context.release();
        }

        for (CLPlatform platform : platforms) {
            platform.release();
        }
    }
}